/**
 * 
 */
package Verifier.FiatShamirProofsVerification;

import java.math.BigInteger;

import Verifier.ArithmeticSystem.GroupElement;
import Verifier.ArithmeticSystem.ProductElement;
import Verifier.ByteTree.ByteTree;
import Verifier.ByteTree.Leaf;
import Verifier.ByteTree.Node;

/**
 * This is an inner interface to represent a proof validation process.
 * 
 * @author Moshe Baavur
 *
 */
abstract class ProofVerifier {
	protected FiatShamirProofInfo proofInfo;
	private byte[] lastComputedSeed;
	protected BigInteger lastComputedChallenge;
	protected BigInteger totalExponent;
	
	/**
	 * Creates a new proof verifier with the included parameter as the input data for the proof.
	 * 
	 * @param info the Fiat-Shamir proof data.
	 */
	public ProofVerifier(FiatShamirProofInfo info) {
		this.proofInfo = info;
		this.lastComputedSeed = null;
		this.lastComputedChallenge = null;
		this.totalExponent = BigInteger.ONE;
	}
	
	/**
	 * Validate the entire proof as the algorithm states in the verifier's document.
	 * 
	 * @return true if and only if the proof is valid according to the algorithm describing the verification.
	 * @throws Exception if a seed cannot be made using the random oracle.
	 */
	public boolean validateProof() throws Exception {
		// first, check that the data is valid
		if (false == checkInputValidity()) {
			return (false);
		}
		
		// try to interpret the commitment
		if (false == interpretCommitment()) {
			// commitment is not in the correct form - reject
			return (false);
		}
		
		// try to interpret the reply
		if (false == interpretReply()) {
			// reply is not in the correct form - reject
			return (false);
		}
		
		// compute the seed for the proof
		computeSeed();
		
		// create the random data using the seed
		createRandomData();
		
		// compute the challenge for the proof
		computeChallenge();
		
		// now, the last step is to validate the calculations
		return (validateCalculations());
	}
	
	/**
	 * Checks that the input is valid, meaning all the needed values are present and valid.
	 * 
	 * @return true if and only if the data is valid.
	 */
	abstract protected boolean checkInputValidity();
	
	/**
	 * Convert the commitment into a set of objects, as described in each algorithm - this is part of step 1 in each of the algorithms.
	 * 
	 * @return true if and only if the commitment was in the correct form.
	 */
	abstract protected boolean interpretCommitment();
	
	/**
	 * Convert the reply into a set of objects, as described in each algorithm - this is part of step 1 in each of the algorithms.
	 * 
	 * @return true if and only if the reply was in the correct form.
	 */
	abstract protected boolean interpretReply();
	
	/**
	 * Computes the seed for the proof - this is step 2 in each of the algorithms.
	 * This method should call createSeedFromByteTree or createSeedFromByteArray to generate the seed.
	 * @throws Exception 
	 * 
	 */
	abstract protected void computeSeed() throws Exception;
	
	/**
	 * Inner method for the inheriting classes to use to compute the seed using a byte tree. The byte tree is concatenated with the prefix,
	 * like all the seeds are. The computed seed is being saved in the seed field.
	 * 
	 * @param dataTree the tree to decode into the seed computation.
	 * @return The computed seed.
	 * @throws Exception when a seed cannot be created due to problem with the RO.
	 */
	protected void createSeedFromByteTree(ByteTree dataTree) throws Exception {
		byte[] seedTreeBytes = dataTree.getByteArray();
		// now, concatenate the byte tree with the prefix and create one new
		// byte array
		byte[] roInput = new byte[proofInfo.prefix().length + seedTreeBytes.length];

		System.arraycopy(proofInfo.prefix(), 0, roInput, 0, proofInfo.prefix().length);
		System.arraycopy(seedTreeBytes, 0, roInput, proofInfo.prefix().length, seedTreeBytes.length);
		// use the one byte array to generate the seed for the proof
		lastComputedSeed = proofInfo.seedsRandomOracle().OutputRandomOracle(roInput);
	}
	
	/**
	 * Inner method for the inheriting classes to use to compute the seed using a byte array. The computed seed is being saved in the seed field.
	 * 
	 * @param dataArray the array to use to compute the seed.
	 * @return The computed seed.
	 * @throws Exception when a seed cannot be created due to problem with the RO.
	 */
	protected void createSeedFromByteArray(byte[] dataArray) throws Exception {
		// use the one byte array to generate the seed
		lastComputedSeed = proofInfo.seedsRandomOracle().OutputRandomOracle(dataArray);
	}
	
	/**
	 * Returns the last computed seed using the create seed from byte tree/array function.
	 * 
	 * @return The last computed seed.
	 */
	protected byte[] getSeed() {
		return (lastComputedSeed);
	}
	
	/**
	 * Creates the random data needed for the proof's verification - this is step 3 in each of the algorithms.
	 */
	abstract protected void createRandomData();
	
	/**
	 * Inner method for the inheriting classes to use to calculate the numbers which should be generated in the 3rd step.
	 * 
	 * @param elementsArray the array to use for the calculations.
	 * @return Element in the world group which is the multiplication of the array's elements after powering them by random numbers.
	 */
	protected GroupElement calculateRandomNumber(GroupElement[] elementsArray) {
		// here we create the array - t_0,...,t_(N-1)
		int bitsPerElement = 8 * (int) Math.ceil(proofInfo.componentBits() / 8.0);
		BigInteger exponent = null;
		totalExponent = BigInteger.ONE;
		BigInteger maxExponent = BigInteger.valueOf(2)
				.pow(bitsPerElement);
		BigInteger modExponent = BigInteger.valueOf(2)
				.pow(proofInfo.componentBits());
		// create a new element - A which is initialized to be the identity
		// element in the group
		GroupElement element;
		if (elementsArray[0] instanceof ProductElement) {
			element = elementsArray[0].getGroup().getGroupIdentityElement();
		} else {
			element = proofInfo.worldGroup().getGroupIdentityElement();
		}
		
		int startBit = 0;
		int endBit = bitsPerElement - 1;
		// iterate through all the elements in the array and
		// randomize an exponent for each and multiply by A
		for (int i = 0; i < elementsArray.length; i++) {
			// calculate the exponent first
			byte[] randomBits = proofInfo.vectorsRandomGenerator().BuildingRandom(
					lastComputedSeed, startBit, endBit);
			// add a zero byte at the start to convert the bits into a non-negative number
			byte[] randomBitsArr = new byte[randomBits.length + 1];
			System.arraycopy(randomBits, 0, randomBitsArr, 1, randomBits.length);
			// need to clear enough bits to get a clean first byte for some reason
			randomBitsArr[1] = (byte)(randomBitsArr[1] & ~(0xFF << (bitsPerElement % 8)));
			exponent = (new BigInteger(randomBitsArr)).mod(maxExponent).mod(modExponent);
			// multiply all the exponents
			totalExponent = totalExponent.multiply(exponent);

			try {
				element = element.multiply(elementsArray[i].power(exponent));
			} catch (Exception e) {
				return (null);
			}

			// move the bits indexes to their next position
			startBit += bitsPerElement;
			endBit += bitsPerElement;
		}
		
		return (element);
	}
	
	/**
	 * Creates the challenge from the data - this is step 4 in each of the algorithms.
	 * Note that all the algorithms work the same for this step.
	 * @throws Exception when a challenge cannot be created due to problem with the RO.
	 * 
	 */
	protected void computeChallenge() throws Exception {
		// compute a challenge v using the RandomOracle
		Node challengeGroupNode = new Node(null);

		Leaf seedLeaf = new Leaf(lastComputedSeed, null);

		// get the root of the commitment tree
		ByteTree commitmentTree = new ByteTree(proofInfo.proofCommitment());
		// the root has a node - this has already been checked
		Node commitmentRoot = (Node)commitmentTree.getFirstByteTreeElement();
		challengeGroupNode.addElement(seedLeaf);
		challengeGroupNode.addElement(commitmentRoot);

		byte[] challengeTreeBytes = ByteTree.byteStringToByteArray(challengeGroupNode.toByteString());
		// now, concatenate the byte tree with the prefix and create one new
		// byte array
		byte[] roInput = new byte[proofInfo.prefix().length + challengeTreeBytes.length];

		System.arraycopy(proofInfo.prefix(), 0, roInput, 0, proofInfo.prefix().length);
		System.arraycopy(challengeTreeBytes, 0, roInput, proofInfo.prefix().length, challengeTreeBytes.length);
		// use the one byte array to generate the seed for the prefix
		byte[] challenge = proofInfo.challengesRandomOracle().OutputRandomOracle(roInput);
		// add a zero byte at the start to convert the bits into a non-negative number
		byte[] challengeArr = new byte[challenge.length + 1];
		System.arraycopy(challenge, 0, challengeArr, 1, challenge.length);
		
		BigInteger maxExponent = BigInteger.valueOf(2).pow(proofInfo.challengeBits());
		// convert the returned array into a number between 0 and 2^n_v (RO_v is
		// set to output data of length n_v, thus this is done perfectly)
		lastComputedChallenge = (new BigInteger(challengeArr)).mod(maxExponent);
	}
	
	/**
	 * Checks that the data we have calculated is valid - this is step 5 in each of the algorithms.
	 * 
	 * @return true if and only if the calculated data is valid.
	 */
	abstract protected boolean validateCalculations();
}
